#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 当前文件的路径是：项目路径/compile_server/compiler.hpp，而util.hpp和log.hpp所在路径是：项目路径/comm/util.hpp，所以要进行相对路径的切换
#include "../comm/util.hpp"
#include "../comm/log.hpp"

// 客户端会提交相应的代码，该模块就负责进行代码的编译

namespace ns_compiler
{
    // 引入路径拼接和日志功能
    using namespace ns_util;
    using namespace ns_log;

    // 实现编译功能
    class Compiler
    {
    public:
        Compiler()
        {}
        ~Compiler()
        {}

        // 返回值：编译成功：true，否则：false
        // 输入参数：编译的文件名
        // file_name: 1234
        // 根据文件类型的不同，该文件名的后缀也不相同，所以我们要分类讨论
        // cpp文件：1234 -> ./temp/1234.cpp 
        // 可执行文件：1234 -> ./temp/1234.exe
        // 报错文件：1234 -> ./temp/1234.stderr
        static bool Compile(const std::string &file_name)
        {
            pid_t pid = fork();
            if(pid < 0)
            {

                LOG(ERROR) << "内部错误，创建子进程失败" << "\n";
                return false;
            }
            else if (pid == 0)
            {
                // 走到这里的肯定是子进程执行流
                umask(0);// umask 是用户创建文件或目录时的默认权限掩码。它的作用是从预设的最大权限里 “移除” 某些权限，以此来确定新创建文件或目录的实际权限。
                // 新创建文件的实际权限是 文件的创建权限 减去 umask 的值，计算公式为：实际权限 = 最大权限 & ~umask

                // 我现在想让编译时的编译报错信息不打印到屏幕上，而是打印到我指定的某个文件中
                // 首先我要创建出这个接受报错信息的文件，然后后面再对标准错误流做一个输出重定向
                int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                // open的参数，第一个是文件路径名，第二个是打开文件的方式，第三个是创建文件时给各用户设置的实际权限

                // 如果open返回值为-1，说明文件打开失败，此时我们就记录对应的日志信息并提前退出
                if(_stderr < 0){
                    LOG(WARNING) << "没有成功形成stderr文件" << "\n";
                    exit(1);
                }
                // 重定向标准错误到_stderr
                // 注意： fd=2原来指向的文件是标准错误流stderr，对应的硬件设备是显示器，现在我不想让它打印到显示器上了，我想让这条错误信息打印到我的文件中
                // 注意，走到这里说明_stderr>0，文件打开成功，此时_stderr里面记录的就是刚刚open系统调用打开文件的文件描述符
                // 因此如果我想让编译时的报错信息打印到我刚刚创建好的_stderr文件中，只需要进行一个输出重定向即可
                dup2(_stderr, 2);// fd=2原来指向的文件是标准错误流stderr，执行这条指令之后，指向的文件就是我们刚刚open打开的文件了
                // 执行这条代码之后，程序运行过程中的所有报错信息都会被输出到_stderr指向的文件中（
                
                // 程序替换，并不影响进程的文件描述符表
                // 子进程: 调用编译器，完成对代码的编译工作
                // linux编译指令：g++ -o target src -std=c++11
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\
                PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE","-std=c++11",  nullptr/*不要忘记*/);
                // execlp参数：第一个是const char *file表示可执行文件名，第二个往后的参数是const char *arg0，表示若干命令行参数，最后以nullptr结尾表示命令行参数传输完毕
                // PathUtil::Exe(file_name)：返回项目路径+file_name.exe
                // PathUtil::Src(file_name)：返回项目路径+file_name.cpp

                // 如果程序替换成功，那这后面的代码是不会执行的
                // 所以如果走到这里，说明程序替换失败了，因此我们就在这里打印一条日志信息提示我们说程序替换失败了
                LOG(ERROR) << "启动编译器g++失败，可能是参数错误" << "\n";
                exit(2);
            }
            else{
                // 走到这里说明是父进程执行流
                // 父进程首先要等待子进程
                waitpid(pid, nullptr, 0);

                // 编译是否成功,就看有没有形成对应的可执行程序
                // 假如说编译器编译成功，那就会形成一个可执行程序，名字就是g++指令中指定的名字（g++ -o + 可执行文件名 + 源文件名）

                // 那如何确定指定路径下是否存在这个可执行文件呢？
                // 我们使用stat函数，传入包含完整路径的可执行文件名，如果存在，stat函数会返回0，如果不存在，stat函数会返回-1

                if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){
                    // 走到这里说明stat函数返回0，可执行文件已经生成，说明编译成功了
                    LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";
                    return true;
                }
            }
            LOG(ERROR) << "编译失败，没有形成可执行程序" << "\n";
            return false;
        }
    };
}